package com.pijiu.compose.demo00

import android.os.Bundle
import android.util.Log
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

/**
 * Jetpack Compose(5)——生命周期与副作用函数
 * 讲解了重组的一些特性，
 * 如何确定重组的范围，
 * 以及重组的中的类型稳定性概念，
 * 以及如何提升非稳定类型在重组过程中的性能。
 *
 * https://www.cnblogs.com/joy99/p/18113704
 */
class Demo03Activity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            Column {
                LifecycleTest()
                ChooseHero()
            }
        }
    }
}

//Composable 组件都是函数，Composable 函数执行会得到一棵视图树，
// 每一个 Composable 组件对应视图树上的一个节点。Composable 的生命周期定义如下：
//
//onActive(添加到视图树) Composable 首次被执行，即在视图树上创建对应的节点。
//onUpdate(重组) Composable 跟随重组不断执行，更新视图树上对应的节点。
//onDispose(从视图树移除) Composable 不再被执行，对应节点从视图树上移除。

//对于 Compose 编写 UI 来说，页面的变化，是依靠状态的变化，Composable 进行重组，
// 渲染出不同的页面。当页面可见时，对应的节点被添加到视图树，当页面不可见时，
// 对应的节点从视图树移除。所以，虽然 Activity 有前后台的概念，但是使用 Compose 编写的页面，
// 对于 Composable 没有前后台切换的概念。当页面切换为不可见时，对应的节点也被立即销毁了，
// 不会像 Activity 或者 Fragment 那样在后台保存实例。

@Composable
fun LifecycleTest() {
    Column {
        Text("LifecycleTest")

        //SideEffect 在每次成功重组的时候都会执行。
        //Composable 在重组过程中会反复执行，但是重组不一定每次都会成功，有的可能会被中断，中途失败。 SideEffect 仅在重组成功的时候才会执行。
        //特点：
        //重组成功才会执行。
        //有可能会执行多次。
        //所以，SideEffect 函数不能用来执行耗时操作，或者只要求执行一次的操作。
        //典型使用场景，比如在主题中设置状态栏，导航栏颜色等。
        SideEffect {
            v("SideEffect")
//            val window = (view.context as Activity).window
//            window.statusBarColor = colorScheme.primary.toArgb()
//            WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme
        }
        //DisposableEffect 可以感知 Composable 的 onActive 和 onDispose, 允许使用该函数完成一些预处理和收尾工作。
        //
        //典型的使用的场景，注册与取消注册：
        DisposableEffect(Unit) { //填Unit只会回调一次，后续不会根据状态变化而变化
            v("onActive")
            // register(callback)
            onDispose {
                v("onDispose")
                // unregister(callback)
            }
        }
    }
}

@Composable
fun ChooseHero() {
    var sheshou by remember {
        mutableStateOf("狄仁杰")
    }

    Column {
        Text(text = "预选英雄： $sheshou")
        Button(onClick = {
            sheshou = "马可波罗"
        }) {
            Text(text = "改选：马可波罗")
        }
        FinalChooseWithCountDown(sheshou)
    }
}

@Composable
fun FinalChoose(hero: String) {
    var tips by remember {
        mutableStateOf("游戏倒计时：10s")
    }
    //LaunchedEffect 用于在 Composable 中启动协程，当 Composable 进入 onAtive 时，
    // LaunchedEffect 会自动启动协程，执行 block 中的代码。当 Composable 进入 onDispose 时，协程会自动取消。
    //同样支持可观察参数，当 key 变化时，当前协程自动结束，同时开启新协程。

    //填Unit只会回调一次，后续不会根据状态变化而变化
//    LaunchedEffect(key1 = Unit) {
//        delay(10000)
//        tips = "最终选择的英雄是：$hero"
//    }

    ////key填hero，可以根据状态变化而触发刷新
    // LaunchedEffect 的参数 key，发生变化时，协程会取消，并重新启动新的协程
    //此时再次执行，在倒计时期间，我们点击 button， 改变预选英雄，结果显示正常了，最终选择的即为马可波罗。
    // 但是该方案并不符合我们的需求，前面讲到， LaunchedEffect 的参数 key，发生变化时，协程会取消，并重新启动新的协程，
    // 这意味着，当倒计时过程中，我们改变了 key ， 重新启动的协程能够获取到改变后的值，但是倒计时也重新开始了，这显然不是我们所期望的结果。
//    LaunchedEffect(key1 = hero) {
//        delay(10000)
//        tips = "最终选择的英雄是：$hero"
//    }

    // 我们把 LaunchedEffect 的参数 key 还原成 Unit。使用 rememberUpdatedState 定义 currentHero。
    //这样，运行结果就符合我们的预期了。
    val currentHero by rememberUpdatedState(newValue = hero)
    //无效，不能显示正确值，虽然可以触发重组，但是重组过程中 LaunchedEffect(key1 = Unit) 中的协程代码并不会执行
//    val currentHero by remember {
//        mutableStateOf(hero)
//    }
    LaunchedEffect(key1 = Unit) { //由于 LaunchedEffect 函数，key 赋值 Unit, 重组过程中，协程代码块并不会重新执行，感知不到外部的变化。
        delay(10000)
        tips = "最终选择的英雄是：$currentHero"
    }

    Text(text = tips)
}

@Composable
fun FinalChooseWithCountDown(hero: String) {
    var time by remember {
        mutableIntStateOf(10)
    }
    //derivedStateOf 的作用是从一个或者多个 State 派生出另一个 State。
    // 如果某个状态是从其他状态对象计算或派生得出的，则可以使用 derivedStateOf。
    // 使用此函数可确保仅当计算中使用的状态之一发生变化时才会进行计算。
    //derivedStateOf 的使用不难，但是和 remember 的配合使用可以有很多玩法来适应不同的场景，
    // 主要的关注点还是在触发重组的条件上，这个要综合实际的场景和性能来觉得是用 key 来触发重组还是改变引用的状态来触发重组。
    val tips by remember {
        derivedStateOf {
            "游戏倒计时：${time}s"
        }
    }
    LaunchedEffect(key1 = Unit) {
        launch {
            repeat(10) {
                delay(1000)
                time--
            }
        }
        launch {
            //前面使用 rememberUpdatedState 可以在 LaunchedEffect 中始终获取到外部状态的最新的值。
            // 但是无法感知到状态的变化，也就是说外部状态变化了，LaunchedEffect 中的代码无法第一时间被通知到。
            // 用 snapshotFlow 则可以解决这个场景。
            //snapshotFlow 用于将一个 State<T> 转换成一个协程中的 Flow。
            // 当 snpashotFlow 块中读取到的 State 对象之一发生变化时，如果新值与之前发出的值不相等，
            // Flow 会向收集器发出最新的值（此行为类似于 Flow.distinctUntilChaned）。
            snapshotFlow { time }.collect {
                v("游戏倒计时：${it}s")
            }
        }
    }
    Text(
        text = if (time == 0) {
            "最终选择的英雄是：$hero"
        } else {
            tips
        }
    )
}


private fun v(msg: String) {
    Log.e("pijiu", msg)
}
